package com.my.netty.nio.net.selector;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

@Slf4j
public class SelectorNioServer {

    /*
        解释：
        1.Selector只有一个，Channel有多个，每注册1个Channel到Selector上会创建一个SelectionKey
        2.SelectionKey表示Selector和1个Channel的关联关系，并指定了Selector关注这个Channel上的哪些事件
        3.每当Channel上发生事件，Selector会把事件对应的SelectionKey加入到selector.selectedKeys()集合中
        4.发生事件后，selector.selectedKeys()集合中的SelectionKey不会被自动移除


        SelectionKey事件共有4种
        1.accept：服务端接收到客户端连接时（服务端）
        2.connect：客户端成功建立连接时（客户端）
        3.read：channel中有数据可读时
        4.write：可以向channel写数据时
     */

    public static void main(String[] args) throws IOException {
        //NIO的非阻塞式TCP服务端+Selector（单线程处理）

        //1.创建Selector；Selector可管理多个channel，并监听每个channel的连接、可读、可写事件
        Selector selector = Selector.open();

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));

        //2.建立Selector和ServerSocketChannel的联系，把Selector注册到Channel上，必须是非阻塞模式才能注册；ops表示默认关注哪些事件，默认0为不关注任何事件
        //返回值SelectionKey可以在发生事件时获取事件类型和发生事件的Channel
        SelectionKey sscKey = ssc.register(selector, 0, null);
        //指定SelectionKey关注的事件类型为accept
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("Selector注册key:{}", sscKey);

        while (true) {
            log.debug("Selector等待事件...");
            //3.select()方法监听selector注册的channel上发生的事件
            //如果没有发生selector关注的事件，则select()方法保持阻塞；如果有发生事件，select向下运行；注意如果selector中还有未处理的事件，则依然不会阻塞
            selector.select();

            //4.处理事件
            //selectedKeys()返回当前所有可用事件集合，遍历
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectionKeys.iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                log.debug("Selector监听到可用事件key{}", key);

                //5.区分事件类型
                if (key.isAcceptable()) {
                    //获取accept事件所在的channel
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    //监听的事件是accept事件，此时可以获取客户端连接
                    SocketChannel sc = channel.accept();
                    log.debug("获取客户端连接:{}", sc.getRemoteAddress());

                    //为了之后处理读事件，需要将SocketChannel注册到selector，注意selector只能工作在非阻塞模式下
                    sc.configureBlocking(false);
                    SelectionKey scKey = sc.register(selector, 0, null);
                    //这个scKey只负责监听SocketChannel的读事件
                    scKey.interestOps(SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    //获取读事件所在的channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buf = ByteBuffer.allocate(16);
                    try {
                        int len = channel.read(buf);
                        if (len != -1) {
                            //打印读取数据
                            buf.flip();
                            CharBuffer charBuffer = Charset.defaultCharset().decode(buf);
                            buf.clear();
                            log.debug("读取客户端数据完成 {} : {}", channel.getRemoteAddress(), charBuffer);
                        } else {
                            log.info("客户端断开连接 {}", channel.getRemoteAddress());
                            //客户端断开链接，会触发一个读事件，read方法读到-1，此时需要将key取消，否则下次select依然会拿到这个读事件
                            key.cancel();
                        }
                    } catch (IOException e) {
                        log.error("客户端强制断开连接 {}", channel.getRemoteAddress(), e);
                        //客户端异常断开，会触发一个读事件，read方法抛出异常，此时需要将key取消，否则下次select依然会拿到这个读事件
                        key.cancel();
                    }
                }

                //处理完一个SelectionKey，需要手动移除，否则下次可能出现NPE
                iter.remove();

                //如果不处理事件（不调用accept(),read(),write()），则需要手动取消SelectionKey，否则下次select()不会阻塞
//                key.cancel();
            }
        }
    }
}
